﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using TV_Organizer.Properties;
using System.Diagnostics;

namespace TV_Organizer.FileScanner
{
    class FileScanner
    {
        private ICollection<FileSystemWatcher> m_fileSystemWacthers = new Collection<FileSystemWatcher>();

        #region Singleton
        private FileScanner()
        {
            this.Initialize();

            if (Settings.Default.FileSystemWatch)
            {
                //m_fileSystemWacther = new FileSystemWatcher();
                //m_fileSystemWacther.
            }
        }

        private static FileScanner m_instance = new FileScanner();
        public static FileScanner Instance
        {
            get { return m_instance; }
        }
        #endregion

        #region props
        public ObservableCollection<string> FoldersToScan { get; set; }
        public ObservableCollection<File> Files { get; set; }
        public bool IsAnalyzing { get; private set; }
        #endregion

        private void Initialize()
        {
            if (Settings.Default.FoldersToScan == null)
                Settings.Default.FoldersToScan = new System.Collections.Specialized.StringCollection();

            this.FoldersToScan = new ObservableCollection<string>();
            this.FoldersToScan.CollectionChanged += FoldersToScan_CollectionChanged;
            foreach (var folder in Settings.Default.FoldersToScan)
                this.FoldersToScan.Add(folder);

            Settings.Default.SettingsSaving += new System.Configuration.SettingsSavingEventHandler(Default_SettingsSaving);

            this.Files = new ObservableCollection<File>();

            this.IsAnalyzing = false;

            Settings.Default.SettingChanging += (o, e) =>
            {
                if (e.SettingName == "FileScannerFileSystemWatch")
                    this.UpdateWatchers();
            };
            this.UpdateWatchers();
        }

        #region Watchers

        private void UpdateWatchers()
        {
            var l_enable = Settings.Default.FileSystemWatch;
            foreach (var watcher in m_fileSystemWacthers)
                watcher.EnableRaisingEvents = l_enable;
        }

        void FoldersToScan_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (var folder in e.NewItems.OfType<string>())
                {
                    if (!Directory.Exists(folder)) return;

                    var l_watcher = new FileSystemWatcher(folder)
                    {
                        IncludeSubdirectories = true,
                        EnableRaisingEvents = true,
                    };
                    l_watcher.Created += new FileSystemEventHandler(l_watcher_Created);
                    m_fileSystemWacthers.Add(l_watcher);
                }
            }

            if (e.OldItems != null)
            {
                foreach (var folder in e.OldItems.OfType<string>())
                {
                    var l_watcher = m_fileSystemWacthers.FirstOrDefault(w => w.Path == folder);
                    if (l_watcher != null)
                    {
                        l_watcher.EnableRaisingEvents = false;
                        m_fileSystemWacthers.Remove(l_watcher);
                    }
                }
            }
        }

        void l_watcher_Created(object sender, FileSystemEventArgs e)
        {
            if (System.IO.File.Exists(e.FullPath))
                this.Files.Add(new File(e.FullPath));
            else if (Directory.Exists(e.FullPath))
                this.ScanFolder(e.FullPath, 0);
        }

        #endregion

        private void Default_SettingsSaving(object sender, System.ComponentModel.CancelEventArgs e)
        {
            Settings.Default.FoldersToScan.Clear();
            foreach (var folder in this.FoldersToScan)
                Settings.Default.FoldersToScan.Add(folder);
        }

        public void Scan()
        {
            App.Instance.Window.DisplayBusyIndicator("Scanning...", "", Visibility.Collapsed);

            foreach (var folder in this.FoldersToScan)
            {
                this.ScanFolder(folder, 0);
            }

            App.Instance.Window.FileScannerControl.ResizeFileScannerColumnsAsync();
            App.Instance.Window.HideBusyIndicator();

            if (Settings.Default.AnalyzeFilesAfterLoad)
                this.AnalyzeAllFilesAsync();
        }

        public void ScanFolder(string folder, int depth)
        {
            if (depth >= Settings.Default.MaxDepth) return;
            if (!Directory.Exists(folder)) return;

            App.Instance.Window.BusyMessage = folder;
            
            var extensions = new List<string>();
            extensions.AddRange(Settings.Default.MediaExtensions.OfType<string>());
            extensions.AddRange(Settings.Default.AttachedFilesExtensions.OfType<string>());

            try
            {
                foreach (var file in Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly))
                {
                    var extension = Path.GetExtension(file);
                    if (extensions.Contains(extension))
                    {
                        Application.Current.Dispatcher.BeginInvoke(new System.Action<string>((filePath) =>
                        {
                            if (this.Files.FirstOrDefault(f => f.Path == filePath) == null)
                                this.Files.Add(new File(filePath));
                        }), new object[] { file });
                    }
                }

                foreach (var dir in Directory.EnumerateDirectories(folder, "*", SearchOption.TopDirectoryOnly))
                {
                    this.ScanFolder(dir, depth + 1);
                }
            }
            catch (UnauthorizedAccessException)
            { }
            catch (PathTooLongException)
            { }
        }

        public Task AnalyzeAllFilesAsync()
        {
            return this.AnalyzeFilesAsync(this.Files);
        }

        public async Task AnalyzeFilesAsync(IEnumerable<File> files)
        {
            if (this.IsAnalyzing)
            {
                LogsManager.Log(this, "Another analyze is executing. This one will be canceled.");
                return;
            }
            this.IsAnalyzing = true;

            var stopWatch = Stopwatch.StartNew();

            App.Instance.Window.ProgressBarMinimum = 0;
            App.Instance.Window.ProgressBarValue = 0;
            App.Instance.Window.ProgressBarMaximum = files.Count();
            App.Instance.Window.DisplayBusyIndicator("Analyzing files", "");
            
            var tasks = new List<Task>();
            foreach (var file in files)
            {
                var task = Task.Factory.StartNew((f) => ((File)f).Analyze(), file);
                task.ContinueWith((t) => Application.Current.Dispatcher.BeginInvoke(new System.Action(() => App.Instance.Window.ProgressBarValue++)));
                tasks.Add(task);
            }
            await TaskEx.Run(() => Task.WaitAll(tasks.ToArray()));

            if (Settings.Default.RenameAttachedFilesAsMediaFile)
            {
                tasks.Clear();
                var attachedFiles = files.Where(f => f.IsAttachedFile).ToList();
                var mediaFiles = files.Where(f => f.IsMediaFile).ToList();

                App.Instance.Window.ProgressBarValue = 0;
                App.Instance.Window.ProgressBarMaximum = attachedFiles.Count();
                App.Instance.Window.DisplayBusyIndicator("Mapping attached files", "");

                foreach (var attachedFile in attachedFiles)
                {
                    var task = attachedFile.FindAndAttachToMediaFileAync(mediaFiles);
                    task.ContinueWith((t) => App.Instance.Window.ProgressBarValue++);
                    tasks.Add(task);
                }
                
                await TaskEx.Run(() => Task.WaitAll(tasks.ToArray()));
            }

            App.Instance.Window.HideBusyIndicator();
            App.Instance.Window.FileScannerControl.ResizeFileScannerColumnsAsync();
            this.IsAnalyzing = false;

            stopWatch.Stop();
            LogsManager.Log(this, "Analyze done. Took {0}ms for {1} files.", stopWatch.ElapsedMilliseconds, files.Count());
            App.Instance.Window.Notify("{0} files analyzed.{1}{2} are ready to be processed.", files.Count(), Environment.NewLine, files.Where(f => f.ProcessFile).Count());
        }

        public void Process()
        {
            try
            {
                this.ProcessAsync(this.Files);
            }
            catch (Exception e)
            {
                var l_xcp = e.GetBaseException();
                MessageBox.Show(string.Format("Processing the files throwed an error. Exception {0}: {1}", l_xcp.GetType().Name, l_xcp.Message), "An error occured", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        public async void ProcessAsync(IEnumerable<File> files)
        {
            var moveTVShows = Settings.Default.MoveTvShows;
            var tvShowsFolder = Settings.Default.TVShowsFolder;

            if (moveTVShows && !Directory.Exists(tvShowsFolder))
                throw new DirectoryNotFoundException(string.Format("Directory {0} was not found. Please specify another TV Shows folder.", tvShowsFolder));

            var tasks = new List<Task>();
            foreach (var file in files.ToList())
            {
                tasks.Add(this.ProcessFileAsync(file));
            }

            await TaskEx.Run(() => Task.WaitAll(tasks.ToArray()));

            App.Instance.Window.Notify("Renamed {0}{1} files", Settings.Default.MoveTvShows ? "and moved " : "", files.Count());
        }

        public Task ProcessFileAsync(File file)
        {
            return TaskEx.Run(() => this.ProcessFile(file));
        }

        private static object _folderDeleteLock = new object();
        private static object _folderCreateLock = new object();
        public void ProcessFile(File file)
        {
            if (!file.ProcessFile) return;

            //App.Instance.Window.WriteToStatusBar("Processing: {0}{1}", file.Filename, file.Extension);

            if (file.IsSample && Settings.Default.RemoveSamples)
            {
                System.IO.File.Delete(file.Path);
                LogsManager.Log(this, string.Format("Removed file: {0}", file.Path));
                return;
            }

            // Pas de déplacement nécessaire => ne rien faire
            if (!file.NeedsMove.HasValue || !file.NeedsMove.Value)
                return;

            // Cible existe => on demande quoi faire
            if (System.IO.File.Exists(file.TargetFilePath))
            {
                var message = string.Format("Could not move file: {1} to {2}. Destination file already exists. Do you want to delete {1} ?{0}{0}Yes: Delete source{0}No: Overwrite destination{0}Cancel: Do nothing",
                    Environment.NewLine, System.IO.Path.GetFileName(file.Path), System.IO.Path.GetFileName(file.TargetFilePath));
                var result = MessageBox.Show(message, "File already exists", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
                switch (result)
                {
                    case MessageBoxResult.Yes:
                        System.IO.File.Delete(file.Path);
                        break;
                    case MessageBoxResult.No:
                        System.IO.File.Delete(file.TargetFilePath);
                        break;
                    case MessageBoxResult.Cancel:
                        return;
                }
            }

            // Répertoire cible n'existe pas => on le crée
            lock (_folderCreateLock)
            {
                if (!Directory.Exists(file.TargetFolder))
                    Directory.CreateDirectory(file.TargetFolder);
            }

            string originalFolder = file.Folder;

            // Déplacement des fichier "normaux"
            if (System.IO.File.Exists(file.Path))
            {
                try
                {
                    System.IO.File.Move(file.Path, file.TargetFilePath);
                }
                catch (Exception e)
                {
                    LogsManager.Log(this, string.Format("Could not move file {0} to {1}. {2} occured with the following message: {3}",
                        file.Path, file.TargetFilePath, e.GetBaseException().GetType().Name, e.GetBaseException().Message));
                    return;
                }

                file.SetPath(file.TargetFilePath);
                file.NeedsMove = false;

                LogsManager.Log(this, string.Format("Moved file: {0} to {1}", file.Path, file.TargetFilePath));
            }

            // Remove empty dirs
            lock (_folderDeleteLock)
            {
                if (Settings.Default.RemoveEmptyDirectories && Directory.Exists(originalFolder))
                    this.DeleteFolderIfEmpty(originalFolder);
            }
        }

        private void DeleteFolderIfEmpty(string folderPath)
        {
            if (Directory.EnumerateFiles(folderPath).Count() == 0 && Directory.EnumerateDirectories(folderPath).Count() == 0)
            {
                Directory.Delete(folderPath);
                LogsManager.Log(this, string.Format("Removed empty directory: {0}", folderPath));

                var parentFolder = Directory.GetParent(folderPath);
                if (parentFolder != null)
                    this.DeleteFolderIfEmpty(parentFolder.FullName);
            }
        }
    }
}
